package it.unibo.imola.parser.visitor.entity;

import java.util.ArrayList;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CallStatement extends Statement {

	private Logger logger = LoggerFactory.getLogger(getClass());
	protected String name;
	protected ToolBox toolbox;
	protected ArrayList<Object> actualParametersList;
	
	public CallStatement(String name, ToolBox toolbox) {
		if(name == null)
			throw new IllegalArgumentException("name == null");
		if(toolbox == null)
			throw new IllegalArgumentException("toolbox == null");
		this.name = name;
		this.toolbox = toolbox;
		actualParametersList = new ArrayList<Object>(0);
	}
	
	public void addActualParameter(Object param){
		if(param == null)
			throw new IllegalArgumentException("param == null");
		actualParametersList.add(param);
	}

	@Override
	public boolean check(Environment env) throws Exception {
		super.check(env);
		
		Class<?>[] actualParameterType = new Class<?>[actualParametersList.size()];
		
		// creo l'array di tipi per vedere se esiste un metodo con questa signature
		// cioè se corrisponde alla definizione dei parametri formali
		//
		// devo cercare sia tra le funzioni che ho definito, sia nella toolbox
		
		for(int i = 0; i< actualParameterType.length; i++){
			if(actualParametersList.get(i) instanceof String){
				String parameter = actualParametersList.get(i).toString();
				if(parameter.startsWith("\"")){ // è una stringa costante
					actualParameterType[i] = String.class;
					continue;
				}
				else{
					if(!env.getVariables().containsKey(parameter)){
						throw new Exception("undeclared variable \""+parameter+"\"");
					}
					Variable var = env.getVariables().get(parameter);
					actualParameterType[i]= var.getValue().getClass();
					continue;
				}
				
			}
			// qui ci arriva sempre se non è un nome di una variabile
			actualParameterType[i] = actualParametersList.get(i).getClass();
		}
		
		// qui vado prima a vedere se c'è una definizione con quel nome e con quei parametri
		if(env instanceof GlobalEnvironment){
			GlobalEnvironment gEnv = (GlobalEnvironment) env;
			if(gEnv.getDefinitions().containsKey(name)){
				StatementSequence statementSequence = gEnv.getDefinitions().get(name);
				Class<?>[] formalParameterTypes = statementSequence.getDefinition().getFormalParameterTypes();
				boolean compatible = true;
				for(int i = 0; i < formalParameterTypes.length; i++){
					if(formalParameterTypes[i] != actualParameterType[i]){
						if(logger.isDebugEnabled())
							logger.debug("formal["+i+"]="+formalParameterTypes[i].getSimpleName()+" not equal to actual["+i+"]="+actualParameterType[i].getSimpleName());
						compatible = false;
					}
				}
				
				if(compatible)
					return true;
			}
		}
		
		if(logger.isDebugEnabled())
			logger.debug("check: GlobalEnvironment does not contains DEFINITION \""+ name +"\"");
		
		if(toolbox.exists(name,actualParameterType))		
			return true;
		
		if(logger.isDebugEnabled())
			logger.debug("ToolBox does not contains CALL \""+ name +"\" "+ArrayUtils.toString(actualParameterType)+" ");
		
		return false;
	}
	
	@Override
	public void exec(Environment env) throws Exception {
		super.exec(env);
		
		// creo un array con i parametri "unboxed" quindi con solo costanti.
		// per fare questo, se il parametro attuale è una costante, resta uguale
		// se invece è una variabile, ci metto una copia del valore attuale
		
		Object[] unboxedActualParameters = new Object[actualParametersList.size()];
				
		for(int i = 0; i< unboxedActualParameters.length; i++){
			if(actualParametersList.get(i) instanceof String){
				String parameter = actualParametersList.get(i).toString();
				
				if(parameter.startsWith("\"")){ // è una stringa costante
					unboxedActualParameters[i] = parameter.substring(1, parameter.length()-1);
				}
				else{
					if(!env.getVariables().containsKey(parameter)){
						throw new Exception("undeclared variable \""+parameter+"\"");
					}
					Variable var = env.getVariables().get(parameter);
					unboxedActualParameters[i]= var.getValue();
				}
			}
			else unboxedActualParameters[i] = actualParametersList.get(i);
		}
		
		// adesso ho tutti i miei parametri attuali "unboxed", e se ho fatto la check() sono sicuro
		// che o nel GlobalEnv o nella ToolBox il metodo da chiamare c'è.
		// vado prima a vedere se la call fa riferimento ad una funzione definita nello script
		
		if(env instanceof GlobalEnvironment){
			GlobalEnvironment gEnv = (GlobalEnvironment) env;
			if(gEnv.getDefinitions().containsKey(name)){
				StatementSequence statementSequence = gEnv.getDefinitions().get(name);				
				statementSequence.setActualParameters(unboxedActualParameters);
				statementSequence.exec(gEnv);
				return;
			}			
		}
		

		if(logger.isDebugEnabled())
			logger.debug("exec: GlobalEnvironment does not contains DEFINITION \""+ name +"\"");
		
		// se non c'è eseguo dal toolbox
		
		toolbox.exec(name,unboxedActualParameters);
		
	}
	

	@Override
	public String toString() {
//		return new ToStringCreator(getClass()).
//				append(name, actualParametersList).toString();
		return name + ArrayUtils.toString(actualParametersList);
	}
}
